<script setup lang="ts">
import { TerminalApi } from 'vue-web-terminal'
import { MarkerType } from '@vue-flow/core'
import ScriptNodeConfForm from './components/script-node-conf-form/script-node-conf-form.vue'

const title = PageTitleConst.EditScript

const isCmdDirConf = (cmdConf: CmdConfType) => isDirConf(cmdConf)

const scriptConfStore = useScriptConfStore()
const cmdConfStore = useCmdConfStore()

const scriptConfSelected = scriptConfStore.getScriptConfSelected()

const [, getIsLoading] = StatusUtil.genStatus<boolean>(false)

const scriptNodes = ref<ScriptNodeConfType[]>([])
let scriptNodesMap = ref<Record<string, ScriptNodeConfType>>({})
const loadScriptNodes = async () => {
  await ElLoadingGlobal({
    options: { text: '加载脚本节点...' },
    isLoading: getIsLoading,
    handler: async () => {
      const res = await api.findScriptNodeConfs({
        selector: { script: scriptConfSelected._id }
      })
      if (!res.status) {
        ElNotificationByResult(res)
        return
      }
      scriptNodesMap.value = {}
      scriptNodes.value = res.data.map((scriptNode: ScriptNodeConfType) => {
        scriptNode.id = scriptNode._id!
        scriptNodesMap.value[scriptNode._id!] = scriptNode
        // 从数据库中加载的脚本节点都为自定义节点
        scriptNode.type = 'custom'
        // 加载的所有节点都默认为正常未执行状态
        scriptNode.data.status = ScriptNodeStatusConst.Normal
        return scriptNode
      })
    }
  })
}
const scriptEdges = ref<ScriptEdgeConfType[]>([])
let scriptEdgesMap: Record<string, ScriptEdgeConfType> = {}
let scriptEdgesSourceMap: Record<string, ScriptEdgeConfType[]> = {}
const loadScriptEdges = async () => {
  await ElLoadingGlobal({
    options: { text: '加载脚本节点边...' },
    isLoading: getIsLoading,
    handler: async () => {
      const res = await api.findScriptEdgeConfs({
        selector: { script: scriptConfSelected._id }
      })
      if (!res.status) {
        ElNotificationByResult(res)
        return
      }
      scriptEdgesMap = {}
      scriptEdgesSourceMap = {}
      scriptEdges.value = res.data.map((scriptEdge: ScriptEdgeConfType) => {
        scriptEdge.id = scriptEdge._id!
        scriptEdge.markerEnd = MarkerType.ArrowClosed
        scriptEdgesMap[scriptEdge._id!] = scriptEdge
        if (!scriptEdgesSourceMap[scriptEdge.source]) scriptEdgesSourceMap[scriptEdge.source] = []
        scriptEdgesSourceMap[scriptEdge.source].push(scriptEdge)
        return scriptEdge
      })
    }
  })
}
const load = async () => {
  await loadScriptNodes()
  await loadScriptEdges()
}
onBeforeMount(async () => {
  await load()
})

const isShowScriptNodeConfEditor = ref(false)
const isAddScriptNodeConf = ref(false)
const scriptNodeConf = ref<ScriptNodeConfType>(genScriptNodeConf())
const handleAddScriptNodeConf = (cmdConf: CmdConfType) => {
  const scriptNodeConfDefault = genScriptNodeConf()
  scriptNodeConf.value = {
    ...scriptNodeConfDefault,
    data: {
      ...scriptNodeConfDefault.data,
      label: cmdConf.name,
      cmd: cmdConf.cmd!,
      color: cmdConf.color
    },
    script: scriptConfSelected._id!
  }
  isAddScriptNodeConf.value = true
  isShowScriptNodeConfEditor.value = true
}
let scriptNodeConfBackup: ScriptNodeConfType | null = null
const handleClickScriptNode = (node) => {
  scriptNodeConfBackup = ObjUtil.copyDeep(node) as ScriptNodeConfType
  scriptNodeConf.value = node
  isAddScriptNodeConf.value = false
  isShowScriptNodeConfEditor.value = true
}
const handleSelectCmd = (cmdConfId: string) => {
  const cmdConf = cmdConfStore.getMap()[cmdConfId]
  if (!cmdConf || isDirConf(cmdConf)) return
  // 如果脚本节点编辑器没有打开，选择命令，则视为新增脚本节点
  if (!isShowScriptNodeConfEditor.value) handleAddScriptNodeConf(cmdConf)
  // 如果脚本节点编辑器已经打开，选择命令，则视为修改脚本节点中的命令相关内容
  else {
    scriptNodeConf.value.data.label = cmdConf.name
    scriptNodeConf.value.data.cmd = cmdConf.cmd!
    scriptNodeConf.value.data.color = cmdConf.color
  }
}
const closeScriptNodeConfEditor = () => (isShowScriptNodeConfEditor.value = false)
const handleCloseScriptNodeConfEditor = () => {
  // 关闭脚本节点编辑器时，如果当前编辑的脚本节点备份还存在，表示修改后的内容没有进行保存，将脚本节点 data 属性恢复到
  // 修改前的状态
  if (scriptNodeConfBackup) scriptNodeConf.value.data = scriptNodeConfBackup.data
  scriptNodeConf.value = genScriptNodeConf()
  scriptNodeConfBackup = null
  closeScriptNodeConfEditor()
}
const handleSaveScriptNodeSuccess = async () => {
  scriptNodeConfBackup = null
  await load()
  handleCloseScriptNodeConfEditor()
}

const handleRemoveScriptNode = async (ids: string[]) => {
  // 删除与要删除节点相关连的所有边
  let res = await api.removeScriptEdgeConfs({
    selector: {
      script: scriptConfSelected._id,
      $or: [{ source: { $in: ids } }, { target: { $in: ids } }]
    }
  })
  if (!res.status) {
    await load()
    ElNotificationByResult(res)
    return
  }
  // 删除节点
  res = await api.removeScriptNodeConfsByIds(ids)
  await load()
  tipByResult(res)
  if (res.status) closeScriptNodeConfEditor()
}

const handleScriptNodeConnect = async (
  source: string,
  target: string,
  sourceHandle?: string | null,
  targetHandle?: string | null
) => {
  // 根据新连接的边的 source 和 target 确定是否已经存在边
  for (const scriptEdge of scriptEdges.value) {
    if (scriptEdge.source === source && scriptEdge.target === target) {
      ElWarningMessage({ message: '请勿重复连接' })
      return
    }
  }
  const scriptEdgeConfDefault = genScriptEdgeConf()
  scriptEdgeConfDefault.source = source
  scriptEdgeConfDefault.target = target
  if (sourceHandle) scriptEdgeConfDefault.sourceHandle = sourceHandle
  if (targetHandle) scriptEdgeConfDefault.targetHandle = targetHandle
  scriptEdgeConfDefault.script = scriptConfSelected._id!
  await api.saveScriptEdgeConfs([scriptEdgeConfDefault])
  await loadScriptEdges()
}

const handleRemoveScriptEdge = async (ids: string[]) => {
  const res = await api.removeScriptEdgeConfsByIds(ids)
  await loadScriptEdges()
  tipByResult(res)
}

const execScriptBtnIconProps = SIconPropsConst.CircleCaretRight
const termRef = ref()
let execQueue: ScriptNodeConfType[] = []
let currentExecIdx = 0
let currentExecScriptNode: ScriptNodeConfType | null = null
let preExecScriptNode: ScriptNodeConfType | null = null
const handleExecScript = async () => {
  if (scriptNodes.value.length === 0) return
  // 设置所有将要执行的节点的状态为待执行节点状态，并从其中找出开始节点，构建脚本节点执行队列
  execQueue = []
  for (const scriptNode of scriptNodes.value) {
    if (!scriptNode.data.start) continue
    // 先写广度优先遍历
    // 找到开始节点后，寻找其后续节点
    const wait2ExecQueue: ScriptNodeConfType[] = [scriptNode]
    while (wait2ExecQueue.length > 0) {
      const wait2ExecScriptNode = wait2ExecQueue.shift()!
      wait2ExecScriptNode.data.status = ScriptNodeStatusConst.WaitExecute
      execQueue.push(wait2ExecScriptNode)
      // 如果当前加入执行队列的节点为结束节点，则该节点的后续节点不需要再加入执行队列
      if (wait2ExecScriptNode.data.end) continue
      const wait2ExecScriptNodeEdges = scriptEdgesSourceMap[wait2ExecScriptNode._id!]
      if (!wait2ExecScriptNodeEdges) continue
      for (const wait2ExecScriptNodeEdge of wait2ExecScriptNodeEdges) {
        wait2ExecQueue.push(scriptNodesMap.value[wait2ExecScriptNodeEdge.target])
      }
    }
  }
  currentExecIdx = 0
  currentExecScriptNode = execQueue[currentExecIdx]
  if (!termRef.value.isConnected()) await termRef.value!.connect()
  else {
    currentExecScriptNode.data.status = ScriptNodeStatusConst.Executing
    TerminalApi.pushMessage(
      scriptConfSelected._id,
      `====== 执行命令：${currentExecScriptNode?.data?.cmd} ======`
    )
    await termRef.value.execCmd(currentExecScriptNode?.data?.cmd)
    // 下一个
    preExecScriptNode = currentExecScriptNode
    currentExecScriptNode = execQueue[++currentExecIdx]
  }
}
const handleCurrentExecScriptNodeDone = async () => {
  if (preExecScriptNode) preExecScriptNode.data.status = ScriptNodeStatusConst.Normal
  if (!currentExecScriptNode) return
  setTimeout(async () => {
    currentExecScriptNode!.data.status = ScriptNodeStatusConst.Executing
    TerminalApi.pushMessage(
      scriptConfSelected._id,
      `====== 执行命令：${currentExecScriptNode?.data?.cmd} ======`
    )
    await termRef.value.execCmd(currentExecScriptNode?.data?.cmd)
    // 下一个
    preExecScriptNode = currentExecScriptNode
    currentExecScriptNode = execQueue[++currentExecIdx]
  }, 1000)
}
</script>

<template>
  <page-side-responsive
    side-title="命令"
    side-right
    :side-right-show="isShowScriptNodeConfEditor"
    @update:side-right-show="
      (value) => {
        isShowScriptNodeConfEditor = value
        handleCloseScriptNodeConfEditor()
      }
    "
    :content-grid="false"
  >
    <template #title>
      <s-text-ellipsis>{{ title }} - {{ scriptConfSelected.name }}</s-text-ellipsis>
    </template>
    <template #actions>
      <reload-btn :click="load" />
    </template>
    <template #side>
      <tree
        filter-placeholder="搜索命令..."
        :data="cmdConfStore.getTree()"
        :click="handleSelectCmd"
      >
        <template #node-float-right="{ nodeData }">
          <div v-if="!isCmdDirConf(nodeData)">
            <add-btn
              collapse
              :btn-props="{ type: 'info', text: true }"
              text="新增节点"
              :click="() => handleAddScriptNodeConf(nodeData)"
            />
          </div>
        </template>
      </tree>
    </template>
    <template #side-right>
      <script-node-conf-form
        v-model:form="scriptNodeConf"
        :is-add="isAddScriptNodeConf"
        :save-success="handleSaveScriptNodeSuccess"
        :remove="handleRemoveScriptNode"
      />
    </template>
    <template #content>
      <div class="w-full h-full flex flex-col">
        <flow
          class="flex-1"
          v-model:nodes="scriptNodes"
          v-model:edges="scriptEdges"
          :node-click="handleClickScriptNode"
          :node-remove="handleRemoveScriptNode"
          :edge-remove="handleRemoveScriptEdge"
          :connect="handleScriptNodeConnect"
        />
        <s-adjustable-container
          :width-adjustable="false"
          height-adjustable
          :height="150"
          adjust-height-trigger-position="top"
        >
          <el-divider class="pb-1" />
          <term
            ref="termRef"
            :name="scriptConfSelected._id"
            :cmd-done-handler="handleCurrentExecScriptNodeDone"
          >
            <template #options-suf="{ collapse }">
              <x-collapse-btn
                :collapse="collapse"
                :btn-props="{ type: 'primary' }"
                :icon-props="execScriptBtnIconProps"
                text="执行脚本"
                :click="handleExecScript"
              />
            </template>
          </term>
        </s-adjustable-container>
      </div>
    </template>
  </page-side-responsive>
</template>

<style scoped></style>
